package de.jonasjelten.jcalc.parser;

import de.jonasjelten.jcalc.api.JCalc;
import de.jonasjelten.jcalc.mathExps.*;
import de.jonasjelten.jcalc.exception.*;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Diese Klasse liefert einen kompletten JMathExp-Baum aus dem eingegebenen Ausdruck zurück.
 * @author Jonas Jelten
 */
public class JParser {

    private final Map<String, JExpressionParser> parserExpressionMap;
    public final JCalc jCalc;

    private void registerExpressionParser(JExpressionParser expressionParser) throws JParserException {
        expressionParser.setParser(this);
        for (String alias : expressionParser.getExpressionAliases()) {
            //TODO: irgendwas stimmt hier noch nicht, weswegen summation/sigma nicht erkannt wird während pi und product erkannt wird.
            //liegt mit nahezu absoluter sicherheit daran dass sumation 2 aliase hat, die sich nicht vertragen. siehe dazu den debug output
            //der in zeile 55 generiert wird.

            //check alias for validity.
            if (parserExpressionAliasAllowed(alias)) {
                JExpressionParser oldExpressionParser = parserExpressionMap.put(alias, expressionParser);
                if (oldExpressionParser != null) {
                    throw new JExpressionParserRegistrationAliasConflictException(alias, expressionParser, oldExpressionParser);
                }
            } else {
                throw new JExpressionParserRegistrationInvalidAliasException(alias, expressionParser);
            }
        }
    }

    private boolean parserExpressionAliasAllowed(String alias) {
        //TODO: check if alias is allowed (only contains allowed characters, has allowed length, etc.)
        //throw exception if not.
        return (!alias.equals(""));
    }

    public Map getParserExpressionMap() {
        return parserExpressionMap;
    }

    public JParser(JCalc jCalc) throws JParserException {
        this.jCalc = jCalc;
        parserExpressionMap = new HashMap<String, JExpressionParser>();

        // register all known expression parsers.
        registerExpressionParser(new JPiParser());
        registerExpressionParser(new JSummationParser());
        registerExpressionParser(new JProductParser());
        registerExpressionParser(new JBinomeKoefficientParser());
        registerExpressionParser(new JPrimeTestParser());

        //System.out.println(parserExpressionMap);
    }

    /**
     * Parsen einer Rechnung zum JMathExp-Baum
     *
     * @param input Die Rechnung zum Parsen
     * @param jcalc
     * @return Root-JMathExpression
     * @throws JCalcException
     */
    public JMathExp parse(String input) throws JParserException {
        //TODO: sache klären: wieso muss die 0 übergeben werden?
        return doDeepSolve(input, 0);
    }

    public JMathExp parseExpression(String expressionAlias, String expressionArgument) throws JParserException {
        JExpressionParser expressionParser = parserExpressionMap.get(expressionAlias);

        if (expressionParser == null) {
            throw new JExpressionParserException("Exception: Expression alias " + expressionAlias + " unknown.");
        }
        ArrayList<String> args = new ArrayList<String>();

        String tmptext = "";
        int klammergrad = 0;
        for (int j = 0; j < expressionArgument.length(); j++) {
            if (expressionArgument.charAt(j) == '(') {
                klammergrad++;
            } else if (expressionArgument.charAt(j) == ')') {
                klammergrad--;
            }
            if (expressionArgument.charAt(j) == ';' && klammergrad == 0) {
                // jump to next arg
                args.add(tmptext);
                tmptext = "";
            } else {
                tmptext += expressionArgument.charAt(j) + "";
            }
        }
        args.add(tmptext); // letzten teil hinzufügen
        if (klammergrad != 0) {
            throw new JBracketException("Bracket error in function "+expressionAlias+"().");
        }

        return expressionParser.parseExpression(args.toArray(new String[0]));
    }

    /**
     * Diese Methode parst einen substring rekursiv und liefert die root-jmathexp zurück.
     *
     * @author Jonas
     * @param substring Mathematischer ausdruck
     * @param searchpos sollte 0 sein
     * @param einsteigen bei welcher operation wird eingestiegen
     * @param ownerWindow das ownerwindow zum infos ausgeben
     * @return the root-jmathexpression
     * @throws JCalcException
     */
    public JMathExp doDeepSolve(String substring, int einsteigen) throws JParserException {

        int klammergrad = 0; // am anfang sind es sicher 0 klammern.
        int klammergradold = klammergrad;


        //einsteigen = 0; // bei welchem zeichen wird eingestiegen


        // Reihenfolge: +- / * ^ ! () Zahl
        // Reihenfolge incl. functions : +- / * ^ ! FUNCTIONS () Zahl
        // vom rootelement aus


        int lastfoundat = 0;
        if (substring.length() > 1 && (substring.charAt(0) == '-' || substring.charAt(0) == '+')) {
            einsteigen = 0;
        }

        switch (einsteigen) {
            case 0:

                ///////////////////////////////////////////////////
                // + -  Handling
                ///////////////////////////////////////////////////
                lastfoundat = -1;
                JMathExpList plusses = new JMathExpList();
                boolean plusfound = false;
                boolean lastonewasneg = false;
                char aktchar = ' ';
                char aktcharbefore = ' ';

                for (int j = 0; j < substring.length(); j++) {
                    aktchar = substring.charAt(j);
                    aktcharbefore = j > 0 ? substring.charAt(j - 1) : ' ';

                    if (aktchar == ')') {
                        klammergrad--;
                    } else if (aktchar == '(') {
                        klammergrad++;
                    }

                    //das +- wird nicht ignoriert wegen vorangehendem */^e
                    boolean isnoausnahme = (j == 0 || (j > 0 && aktcharbefore != 'e' && aktcharbefore != '^' && aktcharbefore != '*' && aktcharbefore != '/'));
                    //+- gefunden
                    boolean ispm = (aktchar == '+' || aktchar == '-');

                    boolean endeerreicht = (plusfound && j == substring.length() - 1);

                    if (klammergrad == 0 && (isnoausnahme || endeerreicht) && (ispm || endeerreicht)) {
                        if (j == substring.length() - 1 && (aktchar == '+' || aktchar == '-')) {
                            throw new JSyntaxException("syntax error found near + ");
                        } else {
                            int str = j;
                            if (j == substring.length() - 1) { // kp warum aber nur mit funktionierts
                                str++;
                            }
                            if (j == 0 && ispm) { // z.b. -8 oder +88 da ist ein +- an stelle 0
                            } else {
                                String part = substring.substring(lastfoundat + 1, str);
                                JMathExp lastone = doDeepSolve(part, 1);
                                if (lastonewasneg) {
                                    lastone = new JNeg(lastone);
                                }
                                plusses.add(lastone);
                            }
                            if (aktchar == '-') {
                                lastonewasneg = true;
                            } else {
                                lastonewasneg = false;
                            }
                            plusfound = true;
                            lastfoundat = j;

                        }
                    }
                }
                if (plusfound) {
                    return new JSum(plusses.getArray());
                }
            // return the JSum
            /////////////////////////////////////////////////////

            case 1:

                ///////////////////////////////////////////////////
                //  / Handling
                ///////////////////////////////////////////////////
                lastfoundat = 0;



                for (int j = substring.length() - 1; j >= 0; j--) {
                    if (substring.charAt(j) == ')') {
                        klammergrad++;
                    } else if (substring.charAt(j) == '(') {
                        klammergrad--;
                    }
                    if (klammergrad == 0 && (substring.charAt(j) == '/')) {
                        if (j == substring.length() - 1 || j == 0) {
                            throw new JSyntaxException("syntax error found near / ");
                        } else {
                            JMathExp divident, divisor;
                            divident = doDeepSolve(substring.substring(0, j), 1);
                            divisor = doDeepSolve(substring.substring(j + 1, substring.length()), 2);
                            return new JDiv(divident, divisor);
                        }
                    }
                }
            // return the JDiv
            /////////////////////////////////////////////////////


            case 2:

                ///////////////////////////////////////////////////
                // * Handling
                ///////////////////////////////////////////////////
                lastfoundat = 0;
                JMathExpList mals = new JMathExpList();
                boolean malfound = false;
                klammergrad = 0;
                for (int j = 0; j < substring.length(); j++) {
                    if (substring.charAt(j) == '(') {
                        klammergrad++;
                    } else if (substring.charAt(j) == ')') {
                        klammergrad--;
                    }
                    if (klammergrad == 0 && (substring.charAt(j) == '*' || (malfound && j == substring.length() - 1))) {
                        if ((j == 0 || j == substring.length() - 1) && substring.charAt(j) == '*') {
                            throw new JSyntaxException("syntax error found near * ");
                        } else {
                            if (malfound) {
                                int j2;
                                if (j == substring.length() - 1) {
                                    j2 = j + 1;
                                } else {
                                    j2 = j;
                                }
                                JMathExp lastone = doDeepSolve(substring.substring(lastfoundat + 1, j2), 3);
                                mals.add(lastone);
                            } else if (j != 0) {
                                JMathExp lastone = doDeepSolve(substring.substring(0, j), 3);
                                mals.add(lastone);
                            }
                            malfound = true;
                            lastfoundat = j;
                        }
                    }
                }
                if (malfound) {
                    return new JMul(mals.getArray());
                }
            // return the JMul
            /////////////////////////////////////////////////////

            case 3:
                ///////////////////////////////////////////////////
                //  ^ Handling
                ///////////////////////////////////////////////////
                lastfoundat = 0;


                klammergrad = 0;
                for (int j = substring.length() - 1; j >= 0; j--) {
                    if (substring.charAt(j) == ')') {
                        klammergrad++;
                    } else if (substring.charAt(j) == '(') {
                        klammergrad--;
                    }
                    if (klammergrad == 0 && (substring.charAt(j) == '^')) {
                        if (j == substring.length() - 1 || j == 0) {
                            throw new JSyntaxException("syntax error found near ^ ");
                        } else {
                            JMathExp basis, exponent;
                            basis = doDeepSolve(substring.substring(0, j), 4);
                            exponent = doDeepSolve(substring.substring(j + 1, substring.length()), 4);
                            return new JPow(basis, exponent);
                        }
                    }
                }




            // return the JPow
            /////////////////////////////////////////////////////

            case 4:
                ///////////////////////////////////////////////////
                // ! Handling
                ///////////////////////////////////////////////////
                klammergradold = klammergrad;

                for (int j = 0; j < substring.length(); j++) {
                    if (substring.charAt(j) == '(') {
                        klammergrad++;
                    } else if (substring.charAt(j) == ')') {
                        klammergrad--;
                    }

                    if (substring.charAt(j) == '!' && klammergrad == klammergradold) {
                        if (j == 0) {
                            throw new JSyntaxException("syntax error found near ! ");
                        }
                        System.out.println(substring.substring(0, j));
                        return new JFac(doDeepSolve(substring.substring(0, j), -1));
                    }
                }
            // return the JFac
            /////////////////////////////////////////////////////

            case 5:
                ///////////////////////////////////////////////////
                // % Handling
                ///////////////////////////////////////////////////
                klammergradold = klammergrad;

                for (int j = 0; j < substring.length(); j++) {
                    if (substring.charAt(j) == '(') {
                        klammergrad++;
                    } else if (substring.charAt(j) == ')') {
                        klammergrad--;
                    }

                    if (substring.charAt(j) == '%' && klammergrad == klammergradold) {
                        if (j == 0) {
                            throw new JSyntaxException("syntax error found near % ");
                        }
                        System.out.println(substring.substring(0, j));
                        return new JProcent(doDeepSolve(substring.substring(0, j), -1));
                    }
                }
            // return the JProcent
            /////////////////////////////////////////////////////

            default:


                //////////////////////////////////////////////////
                // FUNCTIONS HANDLING
                // Hier kommen funktionen wie sin, sigma, usw.
                //////////////////////////////////////////////////



                Pattern functionregex = Pattern.compile("^(\\w{2,20})\\((.*)\\)$");

                Matcher isfunction = functionregex.matcher(substring);
                if (isfunction.matches()) { // we have a function
                    String functionname = isfunction.group(1);
                    String functionargument = isfunction.group(2);

                    JMathExp functionwert = parseExpression(functionname, functionargument);
                    if (functionwert != null) {
                        return functionwert;
                    }
                }

                ///////////////////////////////////////////////////
                //  () Handling
                ///////////////////////////////////////////////////
                klammergradold = klammergrad;
                if (substring.charAt(0) == '(') {

                    int searchpos = 0;
                    while (searchpos <= substring.length() - 1) {
                        if (substring.charAt(searchpos) == '(') {
                            klammergrad++;
                        } else if (substring.charAt(searchpos) == ')') {
                            klammergrad--;
                        }
                        searchpos++;
                    }
                    searchpos--;
                    if (klammergrad == klammergradold) {
                        System.out.println(substring.substring(1, searchpos));
                        return new JBracket(doDeepSolve(substring.substring(1, searchpos), 0));
                    } else {
                        throw new JBracketException();
                    }
                }
                // return the JBracket
                /////////////////////////////////////////////////////


                ///////////////////////////////////////////////////
                // Number Handling
                ///////////////////////////////////////////////////

                String symbollist = "t|g|m|k|h|µ|u|n|p|f";
                String factorlist = "12|9|6|3|2|-6|-6|-9|-12|-15";

                String[] factorsa = factorlist.split("\\|");
                String[] symbolsa = symbollist.split("\\|");
                //gruppen des ausdrucks:
                // aktuelle gruppen: 5/2:zahlvorkomma 3/12:factor 4:zahlnachfaktor
                // 7:nachkommagesamt 8:zahlnachkommaohneperiode 9:zahlnachkommaMITperiodegesamt 10:vorperiodenachkomma
                // 11:periode 13:evaluegesamt 14:evalue 15:is i
                Pattern number = Pattern.compile("^((\\d+)(" + symbollist + ")?(\\d*)|(\\d*)(\\.|\\,)((\\d+)|((\\d*)\\[(\\d+)\\]))(" + symbollist + ")?(e(-?\\d+))?)?(i)?$");
                Matcher isnumber = number.matcher(substring);
                if (isnumber.matches()) {
                    boolean iscomplex;
                    if (isnumber.group(15) != null) {
                        iscomplex = true;
                    } else {
                        iscomplex = false;
                    }
                    String vorkomma = isnumber.group(2) != null ? isnumber.group(2) : isnumber.group(5);
                    String factor = isnumber.group(3) != null ? isnumber.group(3) : isnumber.group(11);
                    String nachfaktor = isnumber.group(4); //z.b. 4k5 -> 5
                    String nachkomma = isnumber.group(8) != null ? isnumber.group(8) : "";
                    String vorperiodenachkomma = isnumber.group(10);
                    String periode = isnumber.group(11);
                    String evalue = isnumber.group(14);

                    String endzahl = vorkomma;


                    int scale = -nachkomma.length();
                    if (evalue != null) {
                        scale = scale + Integer.parseInt(evalue);
                    }

                    /**
                     * t|g|m|k|h|µ|u|n|p|f as factors for the scale
                     */
                    if (factor != null) {
                        int factorscale = 0;
                        for (int i = 0; i < factorsa.length; i++) {
                            if (symbolsa[i].equals(factor)) {
                                factorscale = Integer.parseInt(factorsa[i]);
                                break;
                            }
                        }
                        if (nachfaktor != null) {
                            if (nachfaktor.length() <= factorscale) {
                                for (int i = 0; i < (factorscale - nachfaktor.length()); i++) {
                                    endzahl += "0";
                                }
                                endzahl += nachfaktor;

                            } else {
                                throw new JParserException("Die Zahl nach " + factor + " ist zu groß, (z.B. 2k345 = 2345 aber 2k3456 nicht möglich.)");
                            }
                        } else {
                            scale = scale + factorscale;
                        }

                    }




                    // 0.88[1337] = 0.88+1337/999900
                    if (periode != null) { //Periode eingegeben

                        String periodenrechnung;
                        if (false && periode.matches("9+")) {
                            //TODO: dann aufrunden und nicht exception throwen...
                            throw new JParserException("Sinnlos eine Periode [9] zu haben, 0.[9] = 1 !!");
                        } else {

                            String iscomplexs = "";
                            if (iscomplex) {
                                iscomplexs = "i";
                            }


                            int periodenlength = periode.length();

                            String periodenbruchnenner = "";
                            String vorderperiode = "";

                            for (int i = 0; i < periodenlength; i++) { // soviele 9ner wie Länge der Periodenzahl
                                periodenbruchnenner += "9";
                            }

                            if (vorperiodenachkomma != null && !vorperiodenachkomma.equals("")) {
                                int abkommazahl = vorperiodenachkomma.length();

                                for (int i = 0; i < abkommazahl; i++) { // soviele nullen an bruchnenner anhängen wie vorperiode stellenanzahl
                                    periodenbruchnenner += "0";
                                }


                                vorderperiode = "0." + vorperiodenachkomma + iscomplexs + "+";
                            }
                            // vercomplexifizierung der einzelnen zahlen
                            endzahl += iscomplexs;
                            periode += iscomplexs;


                            periodenrechnung = endzahl + "+" + vorderperiode + periode + "/" + periodenbruchnenner;

                            JMathExp perconstant;
                            if (scale != 0) {
                                try {
                                    perconstant = new JMul(doDeepSolve(periodenrechnung, 0), new JPow(new JConst(10, jCalc), new JConst(-scale, jCalc)));
                                } catch (JMathematicalException ex) {
                                    throw new JParserException(ex.getMessage());
                                }
                            } else {
                                perconstant = doDeepSolve(periodenrechnung, 0);
                            }

                            return perconstant;
                        }


                    } else { // keine periodenzahl
                        endzahl = endzahl + nachkomma;

                        if (nachkomma.equals("") && false) { // macht scheiß...
                            int zcounter = endzahl.length() - 1;
                            int endzeros = 0;
                            while (zcounter >= 0 && endzahl.charAt(zcounter) == '0') {
                                endzeros++;
                                zcounter--;
                            }
                            scale = scale + endzeros;
                            endzahl = endzahl.substring(0, zcounter + 1);
                        }
                        try {
                            return new JConst(new BigInteger(endzahl), scale, iscomplex, jCalc);
                        } catch (JMathematicalException ex) {
                            throw new JParserException(ex.getMessage());
                        }
                    }
                }




                if (klammergrad != 0) {
                    throw new JBracketException();
                }


                throw new JSyntaxException("General error in your syntax. I got '" + substring + "' as fail...");

        }
    }

    static boolean isNumber(char ch) {
        if (ch == '0' || ch == '1' || ch == '2' || ch == '3' || ch == '4' || ch == '5' || ch == '6' || ch == '7' || ch == '8' || ch == '9') {
            return true;
        } else {
            return false;
        }
    }
}
